feat(loop): spend ceilings — stop-after self-disarm, per-issue attempt budget, daily ceiling (#95) - #127
Merged
Conversation
…, daily ceiling (#95) Bounds the autonomous PR loop's aggregate spend, mirroring gh-aw's cost-management design: an armed loop now self-disarms after budget.stop_after_days (default 7, recorded by arm-loop.sh in .claude/state/loop-arming.json, with a lazy fallback init for loops armed before this feature existed); an issue ping-ponging through budget.per_issue_attempts (default 5) advance/feedback dispatches without landing gets refused and labeled needs-human instead of retried forever; and a budget.daily_action_ceiling (default 50) halts new dispatches for the rest of the UTC day, filing/refreshing a single tracking issue, then resumes automatically at midnight. All three are pre-flight checks in loop-tick.sh's STEP 0, before the verdict decision, so a breach skips the spawn-lock side effect entirely; every gh side effect (notify/label/comment/file-issue) is once-guarded and best-effort. The cockpit's Loop health panel now surfaces stop-after countdown, today's action count vs the daily ceiling, and per-issue attempt counts vs the budget. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…use path TESTS reviewer rejected two blocking coverage gaps on the spend-ceilings change (#95): 1. loop-ceilings.test.sh's stubbed-gh assertions for the ceiling notify path only checked `grep -q "^issue create"`, never the `--label backlog` argument. A silent regression to `--label planned` (or a dropped label) would go undetected, and since loop-census.sh treats `planned`-labeled issues as work, that regression would make the loop treat its own budget-exceeded notice as a new work item. Scenarios 2 and 8 now assert the FULL expected `issue create` call including `--label backlog`, plus a negative assertion that `--label planned` is never emitted. 2. budget_notify_issue()'s reuse path (loop-tick.sh:313-318 -- `gh issue view` an existing tracked issue and comment if OPEN, or file fresh if CLOSED) had zero coverage. New scenario 10 drives three ticks against one fixture: first breach files a tracking issue, second breach (issue OPEN) comments on it instead of duplicating, third breach (issue CLOSED) files a fresh one. The fake bot-gh.sh's `issue view` reply is now configurable via FAKE_ISSUE_STATE. Also added a CLAUDE_TODAY override in loop-tick.sh (mirrors cockpit.sh's COCKPIT_NOW pattern), defaulting to `date -u +%Y-%m-%d` when unset, and wired it through the date-sensitive test scenarios (7, 8, 9, 10) to close a narrow UTC-midnight flake window where the test and the script could independently compute a different calendar date. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
robercano
approved these changes
Jul 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #95
Adds cost/spend rails to the autonomous PR loop so an armed loop cannot run unattended forever and a stuck issue cannot ping-pong feedback cycles indefinitely.
What
.claude/state/loop-arming.json, default +7d viabudget.stop_after_days).loop-tick.shSTEP 0 pre-flight emitsaction=none reason=expiredonce expired and posts a one-time "loop disarmed, re-arm" notice. Re-arming clears the marker..claude/state/loop-issue-attempts.json(budget.per_issue_attempts, default 5). On breach: refuse advance, label the PRneeds-humanand comment once..claude/state/loop-daily-ceiling.json(budget.daily_action_ceiling, default 50). On breach: halt (action=none reason=daily-ceiling), file/refresh a singlebacklog-labeled "budget exceeded" issue (neverplanned), auto-reset next calendar day.cockpit.shloop-health panel shows stop-after countdown, today's actions vs ceiling, and per-issue attempt counts.All three limits configurable via
.claude/self/gates.json→budgetwith sane defaults; documented indocs/TOKEN_BUDGET.md.Design notes
loop-tick.sh(before the spawn-lock side effect); the daemon never re-derives the verdict (issue Harden PR-loop tick: deterministic loop-tick.sh with single verdict, in-flight detection, spawn lock #81 contract preserved).bot-gh.sh.CLAUDE_TODAYtest seam added for deterministic daily-reset tests (defaults todate -u).Gates
GATES_FILE=.claude/self/gates.jsonbuild / lint / test all green, includingsmoke-fanout.sh. Newloop-ceilings.test.sh(45 checks) plus cockpit panel tests.Review
Correctness (opus) and tests (sonnet) lenses both APPROVE (consensus all). Tests reviewer mutation-tested the label and reuse-branch assertions to confirm they are load-bearing.
🤖 Generated with Claude Code